package com.nightpiter.view.drawer;

import static com.nightpiter.controller.log.Logger.log;
import static com.nightpiter.controller.log.Logger.logException;
import static com.nightpiter.controller.log.Logger.logProfilerBegin;
import static com.nightpiter.controller.log.Logger.logProfilerEnd;

import java.util.ArrayList;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Handler;

import com.nightpiter.model.MetroModelData;
import com.nightpiter.model.objects.Station;
import com.nightpiter.model.objects.Transfer;
import com.nightpiter.model.objects.Tunnel;
import com.nightpiter.view.Calculator;
import com.nightpiter.view.Calculator.Mode;
import com.nightpiter.view.drawer.dynamic.DynamicMetroDrawer;
import com.nightpiter.view.graphics.Point;
import com.nightpiter.view.graphics.Rect;
import com.nightpiter.view.graphics.factories.LinearGradientFactory;
import com.nightpiter.view.graphics.factories.PaintFactory;
import com.nightpiter.view.graphics.factories.PathDashPathEffectFactory;
import com.nightpiter.view.graphics.factories.PathEffectFactory;
import com.nightpiter.view.graphics.factories.PathFactory;
import com.nightpiter.view.graphics.factories.RadialGradientFactory;
import com.nightpiter.view.graphics.factories.RectFFactory;
import com.nightpiter.view.graphics.factories.RectFactory;
import com.nightpiter.view.graphics.factories.ShaderFactory;
import com.nightpiter.view.graphics.impl.AndroidCanvas;
import com.nightpiter.view.graphics.impl.factories.AndroidLinearGradientFactory;
import com.nightpiter.view.graphics.impl.factories.AndroidPaintFactory;
import com.nightpiter.view.graphics.impl.factories.AndroidPathDashPathEffectFactory;
import com.nightpiter.view.graphics.impl.factories.AndroidPathEffectFactory;
import com.nightpiter.view.graphics.impl.factories.AndroidPathFactory;
import com.nightpiter.view.graphics.impl.factories.AndroidRadialGradientFactory;
import com.nightpiter.view.graphics.impl.factories.AndroidRectFFactory;
import com.nightpiter.view.graphics.impl.factories.AndroidRectFactory;
import com.nightpiter.view.graphics.impl.factories.AndroidShaderFactory;

/**
 * Respond for drawing the map, managing the cache and so on.
 * 
 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
 */
public class MetroDrawerWrapper {

	static {
		LinearGradientFactory.setFactory(new AndroidLinearGradientFactory());
		RadialGradientFactory.setFactory(new AndroidRadialGradientFactory());
		PaintFactory.setFactory(new AndroidPaintFactory());
		PathDashPathEffectFactory
				.setFactory(new AndroidPathDashPathEffectFactory());
		PathEffectFactory.setFactory(new AndroidPathEffectFactory());
		PathFactory.setFactory(new AndroidPathFactory());
		RectFactory.setFactory(new AndroidRectFactory());
		RectFFactory.setFactory(new AndroidRectFFactory());
		ShaderFactory.setFactory(new AndroidShaderFactory());
	}

	/** Singleton instance */
	private static MetroDrawerWrapper instance = new MetroDrawerWrapper();

	/** selected drawer to */
	@SuppressWarnings("unchecked")
	private Class selectedDrawerClass = DynamicMetroDrawer.class;

	/** cache manager */
	private MetroDrawerCache cache = new MetroDrawerCache();

	/** the drawer */
	private MetroDrawer drawer;

	/** selected station */
	private Station selectedStation;

	/** the set of selected tunnels */
	private ArrayList<Tunnel> selectedTunnels = null;

	/** the set of selected transfers */
	private ArrayList<Transfer> selectedTransfers = null;

	/** the set of selected stations */
	private ArrayList<Station> selectedStations = null;

	/** view dimensions */
	@SuppressWarnings("unused")
	private int width, height;

	private Rect bounds = new Rect();

	private Paint paint = new Paint();

	private RedawingListener redawingListener;

	/**
	 * private constructor
	 */
	private MetroDrawerWrapper() {
		paint.setARGB(255, 0, 0, 0);
		paint.setStrokeWidth(1);
		paint.setStrokeCap(Paint.Cap.ROUND);
		init(10, 10);
	}

	/**
	 * @return the instance
	 */
	public static MetroDrawerWrapper getInstance() {
		return instance;
	}

	/**
	 * all class initializations
	 * 
	 * @param width
	 * @param height
	 */
	private void init(int width, int height) {
		this.width = width;
		this.height = height;

		this.cache.init(width, height);
	}

	/**
	 * sets new width and height values
	 * 
	 * @param width
	 * @param height
	 */
	public void setSize(int width, int height) {
		if (width != 0 && height != 0) {
			init(width, height);
			invalidate();
		}
	}

	/**
	 * create new MetroDrawer instance and compute image bounds
	 * 
	 * @param drawerClass
	 * @param modelData
	 * @param mode
	 * @param preparingListener
	 *            listener that would be notified about preparation process
	 */
	public void prepare(final Class<MetroDrawer> drawerClass,
			final MetroModelData modelData, final Mode mode,
			final PreparingListener preparingListener) {
		final Handler handler = new Handler();
		(new Thread() {
			public void run() {
				try {
					handler.post(new Runnable() {
						public void run() {
							if (preparingListener != null) {
								preparingListener.onDrawerPrepareStart();
							}
						}
					});
					internalPrepare(drawerClass, modelData, mode);
					handler.post(new Runnable() {
						public void run() {
							if (preparingListener != null) {
								preparingListener.onDrawerPrepareSuccess();
							}
						}
					});
				} catch (final PrepareFailedException e) {
					logException(e);
					handler.post(new Runnable() {
						public void run() {
							if (preparingListener != null) {
								preparingListener.onDrawerPrepareFailure(e
										.getMessage());
							}
						}
					});

				}
			}
		}).start();
	}

	/**
	 * create new MetroDrawer instance and compute image bounds
	 * 
	 * @param drawerClass
	 * @param modelData
	 * @param mode
	 * @return true if drawer has successfully prepared, false if preparation
	 *         has failed
	 * @throws PrepareFailedException
	 */
	@SuppressWarnings("unchecked")
	private void internalPrepare(Class<MetroDrawer> drawerClass,
			MetroModelData modelData, Mode mode) throws PrepareFailedException {
		logProfilerBegin("preparing the drawer to work with new model");

		if (drawerClass == null) {
			drawerClass = this.selectedDrawerClass;
		}

		MetroDrawer newDrawer = this.drawer;

		try {
			log("instantiating " + drawerClass.getName());
			newDrawer = drawerClass.newInstance();
		} catch (Throwable e) {
			throw new PrepareFailedException(e.getClass().getSimpleName()
					+ " : " + e.getMessage());
		}

		log("preparing new drawer");
		newDrawer.prepare(modelData, mode);
		log("preparing completed");

		this.bounds = newDrawer.getBounds();
		log("model bounds: width = " + bounds.width() + ", height = "
				+ bounds.height());

		this.drawer = newDrawer;
		this.cache.setDrawer(newDrawer, mode);
		this.selectedDrawerClass = drawerClass;
		this.selectedStation = null;
		invalidate();

		logProfilerEnd("prepare View");
	}

	public Rect getBounds() {
		return new Rect(bounds);
	}

	/**
	 * mark cache as invalid
	 */
	public void invalidate() {
		cache.invalidate();
	}

	public void onCacheUpdated() {
		if (redawingListener != null) {
			redawingListener.onImageUpdated();
		}
	}

	/**
	 * Draw the image on the canvas applying the calculator
	 * 
	 * @param canvas
	 *            the canvas to draw the image on
	 * @param calculator
	 *            the calculator to use for coordinates conversion
	 * @param forceRedraw
	 *            force the cache redrawing
	 * @param redrawListener
	 *            the object that would be notified about cache updating
	 */
	public void draw(Canvas canvas, Calculator calculator, boolean forceRedraw,
			RedawingListener redrawListener) {
		this.redawingListener = redrawListener;
		cache.applyCalculator(calculator, forceRedraw);
		canvas.drawBitmap(this.cache.getImage(), null, this.cache
				.getDestinationRect(), null);
		if (selectedStations == null && selectedTransfers == null
				&& selectedTunnels == null) {
			this.drawSelection(canvas, calculator);
		}
	}

	/**
	 * draw the stationSelection
	 */
	private void drawSelection(Canvas canvas, Calculator calculator) {
		if (drawer != null && drawer.isReady()) {
			if (selectedStation != null) {
				drawer.selectStation(selectedStation, AndroidCanvas
						.wrap(canvas), calculator);
			}
		}
	}

	/**
	 * add a station to the selection
	 * 
	 * @param station
	 *            the station to add
	 */
	public void selectStation(Station station) {
		this.selectedStation = station;
	}

	/**
	 * remove a station from the selection
	 */
	public void deselectStation() {
		this.selectedStation = null;
	}

	/**
	 * add route elements to the selection
	 * 
	 * @param transitions
	 *            transitions to add
	 */
	public void selectRoute(ArrayList<Tunnel> selectedTunnels,
			ArrayList<Transfer> selectedTransfers,
			ArrayList<Station> selectedStations) {
		this.selectedTunnels = selectedTunnels;
		this.selectedTransfers = selectedTransfers;
		this.selectedStations = selectedStations;
		invalidate();
	}

	/**
	 * remove transitions from the selection
	 */
	public void deselectRoute() {
		if (selectedStations != null && selectedTransfers != null
				&& selectedTunnels != null) {
			this.selectedTunnels = null;
			this.selectedTransfers = null;
			this.selectedStations = null;
			invalidate();
		}
	}

	/**
	 * 
	 * @param point
	 * @param calculator
	 * @return
	 */
	public Station[] searchStation(Point point, Calculator calculator) {
		if (drawer == null || !drawer.isReady()) {
			return null;
		}
		return drawer.searchStation(point, calculator);
	}

	public MetroDrawer getDrawer() {
		return drawer;
	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	@SuppressWarnings("serial")
	private class PrepareFailedException extends Exception {

		/** standard error message */
		private static final String MESSAGE = "Drawer preparing failed";

		/**
		 * default constructor
		 */
		public PrepareFailedException() {
			super(MESSAGE);
		}

		/**
		 * @param detailMessage
		 */
		public PrepareFailedException(String detailMessage) {
			super(MESSAGE + " : " + detailMessage);
		}

	}

	/**
	 * RedawingListener interface.
	 * 
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	public interface RedawingListener {

		public void onImageUpdated();
	}

	/**
	 * PreparingListener interface. Should be implemented by any class that
	 * would receive notifications about drawer preparing status
	 * 
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	public interface PreparingListener {

		public void onDrawerPrepareStart();

		public void onDrawerPrepareSuccess();

		public void onDrawerPrepareFailure(String reason);

	}

}
